﻿using System.Linq;
using StyleCop;
using StyleCop.CSharp;

namespace Jsl.StyleCop.CSharp.MaintainabilityRules
{
    /// <summary>
    /// Verifies that the components field is correctly disposed.
    /// </summary>
    /// <remarks>
    /// The standard Dispose logic for WinForms is poorly coded. It is common for programmers to put logic inside the single
    /// if statement. This causes none of the stuff to be disposed if the components field is null. This rule checks for
    /// the Dispose method to correctly check if the disposing parameter is true and inside the if check that the
    /// components field is null before disposing it. The only thing that should be in the inner if is the dispose of
    /// the components field.
    /// </remarks>
    public class DisposeComponentsFieldCorrectly : CsRuleBase<SourceAnalyzer>
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="DisposeComponentsFieldCorrectly"/> class.
        /// </summary>
        /// <param name="analyzer">The analyzer.</param>
        public DisposeComponentsFieldCorrectly(RuleAnalyzer analyzer)
            : base(analyzer)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Analyzes the document.
        /// </summary>
        public override void AnalyzeDocument()
        {
            this.Document.WalkDocument(this.AnalyzeElement);
        }

        /// <summary>
        /// Analyzes the element.
        /// </summary>
        /// <param name="element">The element to analyze.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>Always <c>true</c>.</returns>
        public bool AnalyzeElement(CsElement element, CsElement parentElement, object context)
        {
            element.DoIfDisposeOverrideMethod(this.CheckDisposeMethod);

            return true;
        }
        #endregion Public Methods

        #region Private Methods
        private static bool IsComponentsDisposeCall(Statement statement)
        {
            var expressionStatement = statement as ExpressionStatement;
            if (expressionStatement == null)
            {
                return false;
            }

            var methodInvocationExpression = expressionStatement.Expression as MethodInvocationExpression;
            if (methodInvocationExpression == null)
            {
                return false;
            }

            var expression = methodInvocationExpression.Name.Text;
            return (expression == "components.Dispose")
                || (expression == "this.components.Dispose");
        }

        private static bool IsComponentsReference(Expression expression)
        {
            var literalExpression = expression.RemoveThisAccessor() as LiteralExpression;
            return (literalExpression != null) && (literalExpression.Text == "components");
        }

        private void CheckDisposeCall(Statement disposeStatement, CsElement parentElement)
        {
            // Add a violation if it is not in an IF.
            IfStatement parentIf = disposeStatement.ParentIf();
            if (parentIf == null)
            {
                this.AddViolation(
                    parentElement,
                    disposeStatement.LineNumber,
                    RuleAnalyzer.DisposeComponentsFieldCorrectly,
                    "The components.Dispose must be inside this if: if (this.components != null);");
                return;
            }

            // Process the IF statement allowing the disposing parameter check and the components null check.
            var checkedComponentsIsNull = false;
            Statement firstStatementInIf;
            var evaluatedIf = parentIf.TryEvaluateIf(
                out firstStatementInIf,
                expression =>
                {
                    if (expression.IsLiteral("disposing"))
                    {
                        return true;
                    }
                    else
                    {
                        return null;
                    }
                },
                expression =>
                {
                    return expression.EvaluateNotNullCheckExpression(
                        innerExpression =>
                        {
                            if (innerExpression.IsMemberReference("components"))
                            {
                                checkedComponentsIsNull = true;
                                return true;
                            }

                            return null;
                        });
                });

            if (!evaluatedIf || !checkedComponentsIsNull)
            {
                this.AddViolation(
                    parentElement,
                    parentIf.LineNumber,
                    RuleAnalyzer.DisposeComponentsFieldCorrectly,
                    "The components.Dispose must be inside this if: if (this.components != null);");
                return;
            }

            // The dispose call should be the first statement.
            if (!object.ReferenceEquals(firstStatementInIf, disposeStatement))
            {
                this.AddViolation(
                    parentElement,
                    disposeStatement.LineNumber,
                    RuleAnalyzer.DisposeComponentsFieldCorrectly,
                    "There should only be the this.components.Dispose() inside the if statement.");
                return;
            }

            // Allow a following statement that sets components to null.
            var extraStatements = disposeStatement.FollowingStatements()
                .SkipWhile(this.IsSetComponentsToNullStatement);
            if (extraStatements.Any())
            {
                this.AddViolation(
                    parentElement,
                    disposeStatement.LineNumber,
                    RuleAnalyzer.DisposeComponentsFieldCorrectly,
                    "There should only be the this.components.Dispose() inside the if statement.");
                return;
            }
        }

        private void CheckDisposeMethod(Method method)
        {
            method.WalkElement(null, this.StatementVisitor);
        }

        private bool IsSetComponentsToNullStatement(Statement statement)
        {
            var expressionStatement = statement as ExpressionStatement;
            if (expressionStatement == null)
            {
                return false;
            }

            var assignmentStatement = expressionStatement.Expression as AssignmentExpression;
            if (assignmentStatement == null)
            {
                return false;
            }

            return IsComponentsReference(assignmentStatement.LeftHandSide)
                && assignmentStatement.RightHandSide.IsNullLiteral();
        }

        private bool StatementVisitor<T>(Statement statement, Expression parentExpression, Statement parentStatement, CsElement parentElement, T context)
        {
            // Ignore everything except the components.Dispose() method call.
            if (IsComponentsDisposeCall(statement))
            {
                this.CheckDisposeCall(statement, parentElement);
            }

            return true;
        }
        #endregion Private Methods
    }
}